iT邦幫忙

2023 iThome 鐵人賽

DAY 19
0

前言

昨天我們學了怎麼實作排序與搜尋,但搜尋是針對多了欄位進行模糊匹配。如果想要針對特定欄位的話就需要使用篩選(Filter)

安裝 Filter 套件

讓我們來安裝一下做 Filter 的套件吧!

poetry add django-filter

這樣我們就安裝好 django-filter 這個套件了

接著打開 server/settings.py 並修改

# ...... 以上省略 ......

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "rest_framework_simplejwt",
    "drf_spectacular",
+   "django_filters",
    "server.app.common",
    "server.app.todo",
]

# ...... 以下省略 ......

到這邊我們已經安裝好並通知 Django 我們有這個套件了

接著讓我們繼續編輯 server/settings.py

# ...... 以上省略 ......

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
    "DEFAULT_FILTER_BACKENDS": [
        "rest_framework.filters.OrderingFilter",
        "rest_framework.filters.SearchFilter",
+       "django_filters.rest_framework.DjangoFilterBackend",
    ],
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
    "DEFAULT_PAGINATION_CLASS": "server.utils.pagination.PageNumberPagination",
    "PAGE_SIZE": 10,
}

# ...... 以下省略 ......

上方這樣是告訴 DRF 說除了昨天提到的那兩個 Filter 以外我們還要加上 DjangoFilterBackend 這個 Filter

這樣我們就已經完成套件的安裝以及設定了

建立 API 的 Filter

建立基礎 Filter

現在讓我們打開 ViewSet 來設定哪幾個欄位可以被篩選吧!

讓我們編輯 server/app/todo/views.py

# ...... 以上省略 ......

class TaskViewSet(viewsets.ModelViewSet):
    queryset = todo_models.Task.objects.order_by("id")
    serializer_class = todo_serializers.TaskSerializer
    ordering_fields = ("id", "title")
    search_fields = ("title", "description")
+   filterset_fields = ("is_finish",)

# ...... 以下省略 ......

這邊就是告訴 ViewSet 說我們要讓 is_finish 這個欄位可以被篩選

現在設定好後讓我們試試看吧(記得啟動虛擬環境以及 server 唷)

我們使用 GET 方法請求下方網址

假設我們想要搜尋關聯的部分呢?例如想要用標籤的名稱搜尋對應的任務的話,讓我們打開 server/app/todo/views.py 並修改

# ...... 以上省略 ......

class TaskViewSet(viewsets.ModelViewSet):
    queryset = todo_models.Task.objects.order_by("id")
    serializer_class = todo_serializers.TaskSerializer
    ordering_fields = ("id", "title")
    search_fields = ("title", "description")
-   filterset_fields = ("is_finish",)
+   filterset_fields = ("is_finish", "tags__name")

# ...... 以下省略 ......

這樣就是設定說我們要使用 Tag 的 name 來進行篩選,Django 習慣用兩個底線來表達關聯關係,所以這邊是 tags__name 要特別注意一下。

建立特殊搜尋方式的 Filter

剛剛我們建立的是最簡單的 Filter 都是做完整匹配,也是是篩選的值要完全相同,但有時這樣並不能滿足我們,我們可能會希望有一些特殊的功能例如:

  1. 希望可以篩選 ID 大於某數
  2. 希望可以篩選 ID 大於等於某數
  3. 希望可以篩選 ID 小於某數
  4. 希望可以篩選 ID 小於等於某數
  5. 希望可以搜尋 title 包含特定關鍵字的(區分大小寫)
  6. 希望可以搜尋 title 包含特定關鍵字的(不區分大小寫)

讓我們來改改吧!

一樣是打開 server/app/todo/views.py 並修改

# ...... 以上省略 ......

class TaskViewSet(viewsets.ModelViewSet):
    queryset = todo_models.Task.objects.order_by("id")
    serializer_class = todo_serializers.TaskSerializer
    ordering_fields = ("id", "title")
    search_fields = ("title", "description")
-   filterset_fields = ("is_finish", "tags__name")
+   filterset_fields = {
+       "is_finish": ("exact",),
+       "tags__name": ("exact",),
+   }

# ...... 以下省略 ......

現在我們將搜尋欄位定義的方式改一下,變成上方這個樣子,測試一下可以發現剛剛跟剛剛的功能完全相同。

那這樣寫的意思是我們有兩個欄位可以篩選 is_finish 以及 tags__name,而這兩個欄位都接收一個篩選方式是 exact 也就是完整匹配(前面沒寫實預設也是這個篩選方式),換成這個方式的好處是我們可以開始擴充我們的篩選功能已完成上方的需求。

接著讓我們繼續編輯 server/app/todo/views.py

# ...... 以上省略 ......

class TaskViewSet(viewsets.ModelViewSet):
    queryset = todo_models.Task.objects.order_by("id")
    serializer_class = todo_serializers.TaskSerializer
    ordering_fields = ("id", "title")
    search_fields = ("title", "description")
    filterset_fields = {
        "is_finish": ("exact",),
        "tags__name": ("exact",),
+       "id": ("gt", "gte", "lt", "lte"),
+       "title": ("contains", "icontains"),
    }

# ...... 以下省略 ......

上面這邊我們就是新增了欄位的篩選

  • id:
    • gt: 大於
    • gte: 大於等於
    • lt: 小於
    • lte: 小於等於
  • title
    • contains: 包含
    • icontains: 不區分大小寫包含

大家可以用 GET 方法嘗試一下下方幾個的路由

這邊 django-filters 會幫我們將我們設定的篩選方式以及欄位名稱用兩個底線相連所以 id__lt 代表的是 id 小於某數的意思,但大家可能會想說前面不是提到兩個底線是關聯嗎?這點可以不用擔心,套件會負責處理和判斷那段所以如果真的有 tags__name__contains 類似這樣的篩選,也是完全可以的。

P.S. 大家可能會發現 contains 與 icontains 在測試時候看起來都是不區分大小寫,原因是因為我們目前開發使用的資料庫是 SQLite(Django 預設的資料庫是使用 SQLite,大家應該會看到自己電腦上有個檔案名為 db.sqlite3)他剛好不支援區分大小寫的子字串匹配語法,所以看起來效果是都不區分,但是如果後續將資料庫換成其他的例如 MySQL 或 Postgres 等等的就會有效果了,所以在這邊還是將相關的語法教給大家,詳細的說明可以參考官方文件的說明

另外篩選除了單一欄位以外他其實還可以合併使用,例如我們使用 GET 方法請求 http://127.0.0.1:8000/api/todo/tasks?id__lte=8&is_finish=false (多條件用 & 連結)就代表我們想要尋找 id 小於等於 8 而且未完成的任務。

到這邊我們已經完成了我們前面設計的,大家可能會好奇我們要怎麼知道有哪些篩選的方式可以用呢?其實他是從這邊個文件來的,這與 Django 在做 ORM 搜尋時可以使用的語法(下面會提到)是一致的。

ORM 的搜尋語法

這邊我們補充一下前面沒有提到的 ORM 搜尋語法,假設我們希望可以使用 ORM 語法搜尋來尋找標題包含特定關鍵字的可以使用者個查詢方法

Task.objects.filter(title__contains="關鍵字")

P.S. 如果想測試這個語法可以參考 Day08 的方式練習

到這邊大家應該可以看到 title__contains 與我們傳入 API 的是一致的,這就是為什麼 django-filters 這個套件會這樣設計篩選語法的原因,是因為 Django 的 ORM 搜尋語法就是這樣設計的關係。

總結

今天我們學會了如何做 API 的篩選以及特殊篩選。

結束前別忘了檢查一下今天的程式碼有沒有問題,並排版好喔。

ruff check --fix .
black .
pyright .

今天的內容就到這邊了,明天我們會學習如何做更進階的篩選以及客製化,一起期待一下吧。

P.S. 今天的檔案更新可以參考我的 Git Commit 大家可以搭配服用


上一篇
Day18 - API 搜尋與排序
下一篇
Day20 - 進階篩選(Filter)
系列文
Django REST 大冒險:探索精彩紛呈的 API 開發世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言